﻿namespace SharpF2.Services
{
	using System;
	using System.Collections.Generic;
	using System.IO;
	using System.Windows;
	using System.Windows.Threading;
	using ModelView;

	/// <summary>
	/// Watch a directory event
	/// </summary>
	public class DirectoryScanner
	{
		#region Events

		/// <summary>
		/// Event raised on file created or deleted in the watched directory
		/// </summary>
		public event FileSystemEventHandler DirectoryChangeEvent;

		/// <summary>
		/// Event raised on file renamed in the watched directory
		/// </summary>
		public event RenamedEventHandler ObservedFileRenamedEvent;

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the DirectoryScanner class.
		/// </summary>
		public DirectoryScanner()
		{
			directoryFiles = new HashSet<string>();
			observedFiles = new HashSet<string>();
			isStarted = false;
			dispatcher = Application.Current.Dispatcher;
		}

		#endregion

		#region Private Attributes

		/// <summary>
		/// Contains the file path in the watched directory
		/// </summary>
		private readonly HashSet<string> directoryFiles;

		/// <summary>
		/// Contain the Dispatcher, so we can shoot the update event without an exception
		/// </summary>
		private readonly Dispatcher dispatcher;

		/// <summary>
		/// Contains the file path in the observed file contained in the directory
		/// </summary>
		private readonly HashSet<string> observedFiles;

		/// <summary>
		/// The path of the watched folder
		/// </summary>
		private string folderPath;

		/// <summary>
		/// True if the watcher is started, else false
		/// </summary>
		private bool isStarted;

		#endregion

		#region Public Members

		/// <summary>
		/// Add a file to the observed file
		/// </summary>
		/// <param name="filePath">The file path to add</param>
		public void AddFileToObservation(string filePath)
		{
			if (File.Exists(filePath) && Path.GetDirectoryName(filePath).Equals(folderPath))
			{
				observedFiles.Add(filePath);
			}
		}

		/// <summary>
		/// Remove a file from the observed file
		/// </summary>
		/// <param name="filePath">The file path to check</param>
		/// <returns>True if the file path is contained in theobserved files</returns>
		public bool ObserveFile(string filePath)
		{
			return observedFiles.Contains(filePath);
		}

		/// <summary>
		/// Remove a file from the observed file
		/// </summary>
		/// <param name="filePath">The file path to remove</param>
		/// <returns>Return true if the DirectoryScanner doesn't contain any observed file</returns>
		public bool RemoveFileToObservation(string filePath)
		{
			observedFiles.Remove(filePath);
			return observedFiles.Count == 0;
		}

		/// <summary>
		/// Start the FileSystemWatcher witch run on a different thread
		/// </summary>
		/// <param name="filePath">The path of the file contained in the folder to watch</param>
		/// <returns>True if the watcher has started, else false</returns>
		public bool Start(string filePath)
		{
			folderPath = Path.GetDirectoryName(filePath);
			if (isStarted == false && File.Exists(filePath))
			{
				observedFiles.Add(filePath);

				// Create a new FileSystemWatcher and set its properties.
				// Watch for changes in LastAccess and LastWrite times, and
				// the renaming of files or directories.
				var watcher = new FileSystemWatcher
				              	{
				              		Path = folderPath,
				              		NotifyFilter = (NotifyFilters.LastAccess | NotifyFilters.LastWrite
				              		                | NotifyFilters.FileName | NotifyFilters.DirectoryName),
				              		Filter = "*.*"
				              	};

				// Add event handlers.
				watcher.Changed += OnChanged;
				watcher.Created += OnChanged;
				watcher.Deleted += OnChanged;
				watcher.Renamed += OnRenamed;

				// Begin watching.
				watcher.EnableRaisingEvents = true;

				var subFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.TopDirectoryOnly);
				foreach (var subFile in subFiles)
				{
					directoryFiles.Add(subFile);
				}

				isStarted = true;
			}

			return isStarted;
		}

		/// <summary>
		/// Update the InConflict property of a collection of FileItem
		/// Conflict is define as 
		/// first two FileItem having the same OutputFilePath, in this
		/// case the InConflict of both of them will be set to true
		/// second a FileItem having the same OutputFilePath as another
		/// file contained in the folder, but not observed, in this
		/// case the InConflict of the observed FileItem will be set to true
		/// </summary>
		/// <param name="fileItems">The collection of FileItem</param>
		public void UpdateConflicts(ref IEnumerable<FileItem> fileItems)
		{
			var destinationPaths = new Dictionary<String, FileItem>();

			// Check for internal Conflicts
			foreach (var outFile in fileItems)
			{
				outFile.InConflict = false;

				if (destinationPaths.ContainsKey(outFile.OutputFilePath))
				{
					outFile.InConflict = true;
					destinationPaths[outFile.OutputFilePath].InConflict = true;
				}
				else
					destinationPaths.Add(outFile.OutputFilePath, outFile);
			}

			// Check for external Conflicts
			var posibleConflict = new HashSet<string>(directoryFiles);
			foreach (var observedFile in observedFiles)
				posibleConflict.Remove(observedFile);

			foreach (var outFile in fileItems)
				if (posibleConflict.Contains(outFile.OutputFilePath))
					outFile.InConflict = true;
		}

		#endregion

		#region Private Members

		private void DirectoryChange(FileSystemEventArgs e)
		{
			if (null != DirectoryChangeEvent)
				DirectoryChangeEvent(null, e);
		}

		private void FileRenamed(RenamedEventArgs e)
		{
			if (null != ObservedFileRenamedEvent)
				ObservedFileRenamedEvent(null, e);
		}

		/// <summary>
		/// Update the observed FileItems when a file is created or deleted in the watched folder
		/// </summary>
		/// <param name="source">The caller</param>
		/// <param name="e">Parameters of the update</param>
		private void OnChanged(object source, FileSystemEventArgs e)
		{
			switch (e.ChangeType)
			{
				case WatcherChangeTypes.Created:
					directoryFiles.Add(e.FullPath);
					OnDirectoryChangeEvent(e);
					break;
				case WatcherChangeTypes.Deleted:
					directoryFiles.Remove(e.FullPath);

					if (observedFiles.Contains(e.FullPath))
					{
						observedFiles.Remove(e.FullPath);
					}

					OnDirectoryChangeEvent(e);
					break;
				default:
					break;
			}
		}

		/// <summary>
		/// Raise an event signaling clients that an observed file has been created or 
		/// deleted in the watched folder
		/// </summary>
		/// <param name="e">Parameters of the event</param>
		private void OnDirectoryChangeEvent(FileSystemEventArgs e)
		{
			dispatcher.BeginInvoke(
				DispatcherPriority.Normal,
				new DirectoryChangeDelegate(DirectoryChange), e);
		}

		/// <summary>
		/// Raise an event signaling clients that an observed file has been renamed 
		/// in the watched folder
		/// </summary>
		/// <param name="e">Parameters of the event</param>
		private void OnObservedFileRenamedEvent(RenamedEventArgs e)
		{
			dispatcher.BeginInvoke(
				DispatcherPriority.Normal,
				new DirectoryFileRenamedDelegate(FileRenamed), e);
		}

		/// <summary>
		/// Update the observed FileItems when a file is renamed in the watched folder
		/// </summary>
		/// <param name="source">The caller</param>
		/// <param name="e">Parameters of the update</param>
		private void OnRenamed(object source, RenamedEventArgs e)
		{
			switch (e.ChangeType)
			{
				case WatcherChangeTypes.Renamed:
					directoryFiles.Remove(e.OldFullPath);
					directoryFiles.Add(e.FullPath);

					if (observedFiles.Contains(e.OldFullPath))
					{
						observedFiles.Remove(e.OldFullPath);
						observedFiles.Add(e.FullPath);
					}

					OnObservedFileRenamedEvent(e);
					break;
				default:
					break;
			}
		}

		#endregion

		#region Nested type: DirectoryChangeDelegate

		private delegate void DirectoryChangeDelegate(FileSystemEventArgs e);

		#endregion

		#region Nested type: DirectoryFileRenamedDelegate

		private delegate void DirectoryFileRenamedDelegate(RenamedEventArgs e);

		#endregion
	}
}